I know that I earlier tried casting to FinderItem then accessing the URL property, but that it didn't work, either because it said that URL didn't exist, or because it was nil or "". I might have done that in the Xcode debugger rather than in code.
Whatever it was, your solution worked. I have no idea why it didn't work for me when I tried it earlier.
Thanks.
Post
Replies
Boosts
Views
Activity
Thanks for all the info.
I've replaced all uses of anything with "main" to current. See Code section at end.
1
[quote='809385022, DTS Engineer, /thread/766379?answerId=809385022#809385022']
Basically, yes, that's correct. NSMetadataQuery is an older API and, because of that, it's built around the run loop model because that's how basically ALL of our APIs worked.
[/quote]
Is there any newer API that replaces NSMetadataQuery?
2
[quote='809627022, DTS Engineer, /thread/766379?answerId=809627022#809627022']
The key point here is that there isn't any difference between CFRunLoopRun and RunLoop.run
[/quote]
I think that confused me because there's actually a difference between the following two:
CFRunLoopRun()
RunLoop.run()
but not between the following two:
CFRunLoopRun()
RunLoop.run(mode:before:)
as per:
[quote='809786022, DTS Engineer, /thread/766379?answerId=809786022#809786022']
Because of a subtle (but documented) detail of run's implementation. The RunLoop.run() documentation says:
Puts the receiver into a permanent loop, during which time it processes data from all attached input sources.
Now, compare that to the documentation of RunLoop.run(mode:before:):
Runs the loop once, blocking for input in the specified mode until a given date.
The word "Runs the loop once" are the magic words. RunLoop.run(mode:before:) is the "core" API and, in fact, CFRunLoopStop works fine with it. RunLoop.run doesn't return because it's actual swift implementation would (literally) be
[/quote]
Code
I've changed 4 lines from my original code. Each has a numbered // CHANGE comment at the end of the changed line.
Are there any better solutions than the changes that I've made?
import Foundation
import PromiseKit
guard CommandLine.arguments.count > 1 else {
print("Missing adamID argument")
exit(1)
}
guard let adamID = UInt64(CommandLine.arguments[1]) else {
print("adamID argument must be a UInt64")
exit(2)
}
_ = appInfo(forAdamID: adamID)
.done { appInfo in
if let jsonData = try? JSONSerialization.data(withJSONObject: appInfo),
let jsonString = String(data: jsonData, encoding: .utf8)
{
print(jsonString.replacingOccurrences(of: "\\/", with: "/"))
}
CFRunLoopStop(CFRunLoopGetCurrent()) // CHANGE 1
}
CFRunLoopRun() // CHANGE 2
func appInfo(forAdamID adamID: UInt64) -> Promise<[String: Any]> {
Promise { seal in
let query = NSMetadataQuery()
query.predicate = NSPredicate(format: "kMDItemAppStoreAdamID == %d", adamID)
query.searchScopes = ["/Applications"]
var observer: NSObjectProtocol?
observer = NotificationCenter.default.addObserver(
forName: NSNotification.Name.NSMetadataQueryDidFinishGathering,
object: query,
queue: .current // CHANGE 3
) { _ in
query.stop()
defer {
if let observer {
NotificationCenter.default.removeObserver(observer)
}
}
var appInfo: [String: Any] = [:]
for result in query.results {
if let result = result as? NSMetadataItem {
var attributes = ["kMDItemPath"]
attributes.append(contentsOf: result.attributes)
for attribute in attributes {
let value = result.value(forAttribute: attribute)
switch value {
case let date as Date:
appInfo[attribute] = ISO8601DateFormatter().string(from: date)
default:
appInfo[attribute] = value
}
}
}
}
seal.fulfill(appInfo)
}
RunLoop.current.perform { // CHANGE 4
query.start()
}
}
}
Thanks for the info.
Does Launch Services update its database when you trash the app folder, or when you empty the app folder from the trash? (I assume the former)
If Launch Services updates upon trashing, how does Launch Services handle if the app is untrashed via the "Put Back" functionality from the context menu of items in the Trash in Finder? How does it handle if you just drag the app the app folder from Trash back to /Applications (or wherever it was originally located)? What if you drag the app folder from the Trash to some other folder?
I am working on a Swift command-line executable that allows for the uninstallation of apps installed from the Mac App Store.
i’ve seen 3 ways to trash items from Swift without starting a new Process:
Workspace.shared.recycle(…)
FileManager().trashItem(…)
Sending AppleEvents to Finder to trash the item
Are there any other ways that might be better?
The last way includes "Put Back" in the context menus of the trashed items, while the first 2 seem to not include it in the context menus. The last way also follows the standard Finder renaming convention for when an item already exists in the Trash with the same base name as the app folder. The first two use a different renaming scheme than does Finder.
Which of these (or other) Swift-based trashing methods will work with Launch Services & any other things that notice app "uninstallations"?
Since these are apps from the Mac App Store, the app folders are owned by root. My executable thus requires being called as root via sudo (otherwise it couldn't trash the app folders). If the app folders are trashed while the user is root, however, they will be moved to root's Trash, not the user's Trash. To trash the app folders to the user's Trash, it changes the owner of the app folders being uninstalled to the running user (discovered from the $SUDO_USER environment variable), then setuids to that user, then trashes the app folder, then setuids back to root, then changes the owner of the trashed app folders back to root (because Mac App Store apps trashed via Finder remain owned by root in the user's Trash). Will that procedure cause any problems for Launch Services or other things that observe "uninstalls"? Is there any better way to do this?
Lastly, is there any way for Swift to empty from the user's Trash only the app folders that my executable just trashed? I don't want to empty all of the trash in case the user has other items in it.
Thanks.
Thanks for the info.
If I start the run loop with CFRunLoopRun(), then CFRunLoopStop(…) stops the loop and lets my command-line executable exit.
If I start the run loop with RunLoop.main.run(), then CFRunLoopStop(…) doesn't stop the loop, so my command-line executable never exits.
All 3 of the following stopping commands seem to work with CFRunLoopRun(), but none of them works with RunLoop.main.run():
CFRunLoopStop(runLoop)
CFRunLoopStop(RunLoop.main.getCFRunLoop())
CFRunLoopStop(CFRunLoopGetCurrent())
Also, both of the following starting commands seem to work with CFRunLoopRun(), but neither of them works with RunLoop.main.run():
RunLoop.main.perform {…}
DispatchQueue.main.async {…}
Why does CFRunLoopRun(), but not RunLoop.main.run(), get stopped by CFRunLoopStop(…)?
It would make sense to me if RunLoop.main.run() wasn't stopped by any of them if CFRunLoopStop(RunLoop.main.getCFRunLoop()) didn't stop but CFRunLoopStop(CFRunLoopGetCurrent()) did, as maybe those are 2 different CFRunLoops, but I would assume that RunLoop.main & RunLoop.main.getCFRunLoop() use the same CFRunLoop underneath…
(When I say "works" above, I mean that CFRunLoopStop(…) actually lets my executable exit; if it doesn't work, the executable never returns)
Thanks for the info.
I read about CFRunLoopGetCurrent() online, so I updated the code around the call to appInfo(forAdamID:) to be the code at the end of this reply. The new version doesn't call exit(0) and doesn't interfere with my existing testing. Note that the rest of the code remains the same, so I still use the main DispatchQueue in appInfo(forAdamID:).
Is there anything wrong with doing this instead of using RunLoop.main.run() & exit(0)?
Is there anything inconsistent with using CFRunLoopGetCurrent() while using the main DispatchQueue in appInfo(forAdamID:)?
If it is inconsistent, how can I fix the inconsistencies?
Is there a current DispatchQueue? Or a way to get the DispatchQueue from the CFRunLoopGetCurrent()?
let runLoop = CFRunLoopGetCurrent()
_ = appInfo(forAdamID: adamID)
.done { appInfo in
CFRunLoopStop(runLoop)
if let jsonData = try? JSONSerialization.data(withJSONObject: appInfo),
let jsonString = String(data: jsonData, encoding: .utf8)
{
print(jsonString.replacingOccurrences(of: "\\/", with: "/"))
}
}
CFRunLoopRun()